use std::{
    env,
    io::Cursor,
    path::{Path, PathBuf},
    sync::LazyLock,
};

use base64::{engine::general_purpose, Engine as _};
use dashmap::DashMap;
use file_icon_provider::get_file_icon;
use image::{DynamicImage, ImageFormat, RgbaImage};
use serde::{Deserialize, Serialize};
use tokio::{fs, process::Command};

// Stores found file type icon base64 string based on its extension.
// Then we don't have to create base64 everytime for same types.
static FILE_TYPE_ICON_BASE64_MAP: LazyLock<DashMap<String, String>> =
    LazyLock::new(|| DashMap::new());

const LOCAL_SETTING_FILENAME: &str = "app-launch-pad.json";

#[derive(Debug, Default, Deserialize, Serialize)]
#[serde_with::serde_as]
pub struct LocalApp {
    #[serde(rename = "fileFullName")]
    file_full_name: String,

    #[serde(rename = "filePath")]
    file_path: String,

    title: String,

    #[serde(rename = "isPinned")]
    is_pinned: bool,

    #[serde(rename = "isPathBroken")]
    is_path_broken: Option<bool>,

    #[serde(rename = "iconBase64")]
    icon_base64: Option<String>,
}

#[derive(Debug, Default, Deserialize, Serialize)]
#[serde_with::serde_as]
pub struct LocalSetting {
    #[serde(rename = "appList")]
    app_list: Vec<LocalApp>,
}

pub async fn read_local_setting() -> Option<LocalSetting> {
    if let Some(setting_file_path) = get_local_setting_path() {
        // Checks if the setting file exists.
        if setting_file_path.exists() {
            // Reads the setting file content.
            if let Ok(file_content) = fs::read_to_string(setting_file_path).await {
                // Parses the file content as JSON.
                if let Ok(mut local_setting) = serde_json::from_str::<LocalSetting>(&file_content) {
                    local_setting.app_list.iter_mut().for_each(|app| {
                        // Gets the file path from the app.
                        let file_full_name = Path::new(&app.file_full_name);

                        app.is_path_broken = Some(!check_path_existence(file_full_name));

                        // Gets the file icon as base64 encoded string.
                        let base64 = get_file_icon_base64(file_full_name);

                        // Sets the icon base64 string to the app.
                        app.icon_base64 = Some(base64);
                    });

                    return Some(local_setting);
                }
            }
        }
    }

    None
}

/// Opens given path and returns error message if failed.
pub async fn open_path(path: String) -> String {
    // Gets Path struct from the given path string.
    let path_struct = Path::new(&path);

    // Checks if the path exists.
    if path_struct.exists() {
        let os = env::consts::OS;

        // Executes the appropriate command to open the path in the user's system.
        let status = match os {
            "windows" => Command::new("explorer").arg(&path).status().await,
            "macos" => Command::new("open").arg(&path).status().await,
            "linux" => Command::new("xdg-open").arg(&path).status().await,
            _ => return format!("Unsupported OS: {}", os),
        };

        // Returns the result of the command execution.
        match status {
            Ok(_) => String::new(),
            Err(err) => format!("Error executing command: {}", err),
        }
    } else {
        format!("File not found: {}", path)
    }
}

pub async fn check_path_type(path: &Path) -> String {
    match fs::metadata(path).await {
        Ok(metadata) => {
            if metadata.is_dir() {
                return String::from("dir");
            } else if metadata.is_file() {
                return String::from("file");
            } else {
                return String::new();
            }
        }
        Err(_) => String::new(),
    }
}

pub fn check_path_existence(path: &Path) -> bool {
    path.exists()
}

pub async fn save_local_setting(local_setting: LocalSetting) {
    // Gets the path to the setting file.
    if let Some(setting_file_path) = get_local_setting_path() {
        // Converts the local setting to JSON string.
        if let Ok(json_string) = serde_json::to_string_pretty(&local_setting) {
            // Writes the JSON string to the setting file.
            let _ = fs::write(setting_file_path, json_string).await;
        }
    }
}

/// Gets file icon and returns as base64 encoded string and mime type.
pub fn get_file_icon_base64(file_path: &Path) -> String {
    // Gets file extension which is key for cached base64 encoded string.
    let extension = file_path.extension();

    // If file extension is found. Just just case files wihout extension.
    if let Some(extension) = extension {
        // Stores base64 string content into map by its extension as key.
        let mut map_key = extension.to_string_lossy().to_lowercase();

        // If this is executable file, we need to use full path as key.
        if extension.eq_ignore_ascii_case("exe") {
            map_key = file_path.to_string_lossy().to_lowercase();
        }

        if let Some(base64) = FILE_TYPE_ICON_BASE64_MAP.get(&map_key) {
            // If file extension is found in cached base64 encoded string, returns it.
            return base64.to_owned();
        } else {
            // Gets the file icon (size 32).
            let icon = get_file_icon(file_path, 32);

            if let Ok(icon) = icon {
                let image = RgbaImage::from_raw(icon.width, icon.height, icon.pixels)
                    .map(DynamicImage::ImageRgba8);

                if let Some(image) = image {
                    // Prepares png format bytes.
                    let mut png_bytes: Cursor<Vec<u8>> = Cursor::new(Vec::new());

                    // Tries to write the image to png format bytes.
                    if image.write_to(&mut png_bytes, ImageFormat::Png).is_ok() {
                        // Converts the png bytes to base64 encoded string.
                        let base64 = general_purpose::STANDARD.encode(&png_bytes.into_inner());

                        // Caches the base64 encoded string for the file extension.
                        FILE_TYPE_ICON_BASE64_MAP.insert(map_key, base64.clone());

                        return base64;
                    }
                }
            }
        }
    }

    String::new()
}

fn get_local_setting_path() -> Option<PathBuf> {
    // Gets current executable file path.
    if let Ok(current_exe) = env::current_exe() {
        // Gets executable file path parent directory path.
        if let Some(executable_path) = current_exe.parent() {
            // Prepares the full path to the setting file.
            let setting_file_path = executable_path.join(LOCAL_SETTING_FILENAME);

            return Some(setting_file_path);
        }
    }

    None
}
